using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using NUnit.Framework;
using Rubikev.Common;
using Rubikev.Common.Data;
using Rubikev.Common.DependencyInjection;

namespace Rubikev.Test.Common
{
   /// <summary>
   /// Class for carrying out all CRUD operations for an entity
   /// Override to ensure that you can Create, Read, Update and Delete an entity
   /// </summary>
   /// <typeparam name="TEntity">The type of entity under test</typeparam>
   public abstract class EntityCrudTestBase<TEntity> 
      where TEntity : class, IEntity, new()
   {
      [TestFixtureSetUp]
      public void TestFixtureSetUp()
      {
         RegisterTypes();
         CreateReferenceData();
      }

      [TestFixtureTearDown]
      public void TestFixtureTearDown()
      {
         CleanUpReferenceData();
      }

      /// <summary>
      /// Set up any reference data required for the tests
      /// </summary>
      protected virtual void CreateReferenceData()
      {
      }

      /// <summary>
      /// Clean up any reference data that was created for the tests
      /// </summary>
      protected virtual void CleanUpReferenceData()
      {
      }

      #region Abstract Methods

      /// <summary>
      /// Setup any dependencies
      /// </summary>
      protected abstract void RegisterTypes();

      /// <summary>
      /// Gets the type provider
      /// </summary>
      protected abstract ITypeProvider TypeProvider { get; }

      /// <summary>
      /// Creates a valid entity
      /// </summary>
      /// <returns>The valid entity</returns>
      protected abstract TEntity CreateValidEntity();

      /// <summary>
      /// Verify that the saved entity's properties are correct
      /// </summary>
      /// <param name="createdEntity">The saved entity</param>
      protected abstract void VerifyCreatedEntityProperties(TEntity createdEntity);

      /// <summary>
      /// Edit the entity, ensuring all of its fields are still valid
      /// </summary>
      /// <param name="original">The original entity</param>
      /// <returns>The edited entity</returns>
      protected abstract TEntity EditEntityToValidState(TEntity original);

      /// <summary>
      /// Verify that the edited entity's properties are correct
      /// </summary>
      /// <param name="editedEntity">The saved entity</param>
      protected abstract void VerifyEditedEntityProperties(TEntity editedEntity);
      
      /// <summary>
      /// Edits the entity to each possible type of invalid entity
      /// </summary>
      /// <param name="original">The original, valid entity</param>
      /// <returns>Each possible type of invalid entity</returns>
      protected abstract IEnumerable<TEntity> EditEntityToInvalid(TEntity original);

      #endregion

      /// <summary>
      /// Get the DAO for an entity
      /// </summary>
      protected IDao<T> CreateDaoAndBeginTransaction<T>() where T : IEntity
      {
         var ret = TypeProvider.GetInstance<IDao<T>>();
         ret.Session.BeginTransaction();
         return ret;
      }

      /// <summary>
      /// Disposes the dao's session
      /// </summary>
      /// <param name="dao">The current dao</param>
      protected void CommitTransactionAndCloseSession<T>(IDao<T> dao) where T : IEntity
      {
         try
         {
            dao.Session.CommitTransaction();
         }
         finally
         {
            dao.Session.Close();
         }
      }

      /// <summary>
      /// Any additional properties to eagerly load on the entity when retrieving it for verification
      /// </summary>
      protected virtual IEnumerable<Expression<Func<TEntity, object>>> PropertiesToFetch{get {yield break;}}

      /// <summary>
      /// Creates and saves a reference entity
      /// </summary>
      /// <param name="entity">The entity to save. If null, uses the <see cref="CreateValidEntity"/> method</param>
      /// <returns>The created entity</returns>
      private TEntity SaveEntity(TEntity entity = null)
      {
         if (entity == null)
         {
            entity = CreateValidEntity();
         }
         var saveDao = CreateDaoAndBeginTransaction<TEntity>();
         var savedEntity = saveDao.AddOrUpdate(entity);
         addedEntityIds.Add(savedEntity.Id);
         CommitTransactionAndCloseSession(saveDao);

         return entity;
      }

      /// <summary>
      /// Retrieves the entity with the given ID from the database
      /// </summary>
      /// <param name="id">The id of the entity to get</param>
      /// <returns>The entity with the given ID, or null</returns>
      protected TEntity RetrieveEntity(int id)
      {
         var retrieveDao = CreateDaoAndBeginTransaction<TEntity>();
         var retrievedEntity = retrieveDao.Get(e => e.Id == id, PropertiesToFetch.ToArray()).SingleOrDefault();
         CommitTransactionAndCloseSession(retrieveDao);
         return retrievedEntity;
      }

      #region Tests

      [Test]
      public void CanCreateValidEntity()
      {
         var savedEntity = SaveEntity();
         Assert.Greater(savedEntity.Id, 0);

         var retrievedEntity = RetrieveEntity(savedEntity.Id);
         VerifyCreatedEntityProperties(retrievedEntity);
      }

      [Test]
      public void CanDeleteEntity()
      {
         var savedEntity = SaveEntity();

         var deleteDao = CreateDaoAndBeginTransaction<TEntity>();
         deleteDao.Delete(savedEntity);
         CommitTransactionAndCloseSession(deleteDao);

         VerifyDeletedEntity(savedEntity);
         addedEntityIds.Remove(savedEntity.Id);
      }

      [Test]
      public void CanEditEntity()
      {
         var savedEntity = SaveEntity();

         var editedEntity = EditEntityToValidState(savedEntity);
         var savedEditedEntity = SaveEntity(editedEntity);

         var retrievedEntity = RetrieveEntity(savedEditedEntity.Id);
         VerifyEditedEntityProperties(retrievedEntity);
      }

      [Test]
      public void CreateInvalidEntity_ThrowsInvalidEntityException()
      {
         foreach (var invalidEntity in EditEntityToInvalid(new TEntity()))
         {
            var dao = CreateDaoAndBeginTransaction<TEntity>();
            var current = invalidEntity;
            Assert.Throws<InvalidEntityException>(() => dao.AddOrUpdate(current));
            dao.Session.Close();
         }
      }

      [Test]
      public void EditEntityToInvalid_ThrowsInvalidEntityException()
      {
         var validEntity = SaveEntity();

         foreach (var invalidEntity in EditEntityToInvalid(validEntity))
         {
            var dao = CreateDaoAndBeginTransaction<TEntity>();
            var current = invalidEntity;
            dao.AddOrUpdate(current);
            Assert.Throws<InvalidEntityException>(() => CommitTransactionAndCloseSession(dao));
         }
      }

      #endregion

      /// <summary>
      /// Check that the entity has been deleted (and that any cascading has happened correctly, etc.)
      /// </summary>
      /// <param name="deletedEntity">The entity that was created then deleted</param>
      protected virtual void VerifyDeletedEntity(TEntity deletedEntity)
      {
         var result = RetrieveEntity(deletedEntity.Id);
         Assert.IsNull(result);
      }

      /// <summary>
      /// Track the added entity IDs so that we can remove them in the TearDown method, after each test
      /// </summary>
      private readonly List<int> addedEntityIds = new List<int>();

      [TearDown]
      public void TearDown()
      {
         var dao = CreateDaoAndBeginTransaction<TEntity>();
         foreach (var id in addedEntityIds)
         {
            int currentId = id;
            var entity = dao.Get(e => e.Id == currentId, PropertiesToFetch.ToArray()).SingleOrDefault();
            dao.Delete(entity);
         }

         addedEntityIds.Clear();
         CommitTransactionAndCloseSession(dao);
      }
   }
}